﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Noris.Schedule.Support.DataFace;
using Noris.Schedule.Support.Sql;
using System.Data.SqlTypes;
using System.Drawing;
using Noris.Schedule.Support.Core;

namespace Noris.Schedule.Support.Expressions
{
    /// <summary>
    /// SchedulerConfigData : podpora konfigurovaných textů ze strany databázového úložiště záznamů, jejich načítání, vyhledání a invalidace.
    /// Paměť záznamů a výkonný blok metod.
    /// Statický přístup k privátnímu Singletonu.
    /// </summary>
    public class SchedulerConfigData
    {
        #region Singleton, Instanční data
        protected static SchedulerConfigData Current
        {
            get
            {
                if (_Current == null)
                {
                    lock (_CreatingLocker)
                    {
                        if (_Current == null)
                        {
                            _Current = _CreateCurrent();
                        }
                    }
                }
                return _Current;
            }
        }
        /// <summary>
        /// Vrací příznak, zda existuje instance Current bez toho, aby se tato založila.
        /// Property Current totiž způsobí, že dojde k vytvoření instance Current a k načtení dat z SQL.
        /// Pokud metoda (typicky static Invalidate()) chce prověřit existenci singletonu, pak to provede přes CurrentExists.
        /// </summary>
        protected static bool CurrentExists { get { return (_Current != null); } }
        private SchedulerConfigData()
        {
			this.Records = new Dictionary<ConfigKey, ConfiguredTexts>();
        }
        /// <summary>
        /// Vytvoří (jedinou) instanci SchedulerConfigData, načte do ní výchozí data a vrátí ji.
        /// Volá se z accessoru get_Current().
        /// </summary>
        /// <returns></returns>
        private static SchedulerConfigData _CreateCurrent()
        {
            SchedulerConfigData current = new SchedulerConfigData();
            current._AddData(SchedulerConfigReadOnlyCls.SqlSelect + " WHERE skupina = " + (Noris.Schedule.Support.Expressions.Constants.CONFIG_GROUP_TEXT).ToString(), true);     // Přednačtu všechny položky skupiny 1 (Konfigurované texty) do paměti
            return current;
        }
        private static object _CreatingLocker = new object();
        private static SchedulerConfigData _Current;
		/// <summary>
		/// Číslo verze dat naposledy načtených.
		/// Každé načítání dat nejprve inkrementuje verzi, a pak s touto verzí přidává nová data.
		/// </summary>
		protected int Version;
		/// <summary>
		/// Soupis dat. 
		/// Klíč Dictionary = složená hodnota (Skupina+Klic).
		/// Hodnota Dictionary = sada textů pro jednu příležitost, rozdělená do property, opatřená číslem verze.
		/// </summary>
		protected Dictionary<ConfigKey, ConfiguredTexts> Records;
        #endregion
        #region Public static metody
		/// <summary>
		/// Vrátí true, pokud pro danou skupinu a klíč dat je uvedená verze dat stále platná.
		/// Více viz property ConfiguredTexts.Version.
		/// </summary>
		/// <param name="skupina"></param>
		/// <param name="klic"></param>
		/// <param name="version"></param>
		/// <returns></returns>
		public static bool IsValid(int skupina, string klic, int version)
		{
			return Current._IsValid(new ConfigKey(skupina, klic), version);
		}
        /// <summary>
        /// Najde (v paměti / v SQL serveru) data pro daný klíč a skupinu, a vrátí je jako seznam.
        /// Pokud data neexistují, vrátí prázdný list (aby bylo možno jej enumerovat bez testů).
        /// </summary>
        /// <param name="klic">Klíč. Typicky 1=konfigurované texty.</param>
        /// <param name="skupina">Skupina. Je daná konkrétním případem (vizuální element).</param>
        /// <returns></returns>
		public static ConfiguredTexts Get(int skupina, string klic)
        {
            return Current._GetData(new ConfigKey(skupina, klic));
        }
        /// <summary>
        /// Zneplatní (zahodí) data daného klíče.
        /// Při jejich příštím použití (požadavku na ně) dojde k jejich donačtení.
        /// Pokud je tato metoda volaná před prvním použitím paměti, tak se (jen kvůli invalidaci) instance v paměti nevytváří.
        /// </summary>
        /// <param name="klic">Klíč. Typicky 1=konfigurované texty.</param>
        /// <param name="skupina">Skupina. Je daná konkrétním případem (vizuální element).</param>
        public static void Invalidate(int skupina, string klic)
        {
            if (CurrentExists)
                Current._Invalidate(new ConfigKey(skupina, klic));
        }
        /// <summary>
        /// Zneplatní (zahodí) data daného klíče.
        /// Při jejich příštím použití (požadavku na ně) dojde k jejich donačtení.
        /// Pokud je tato metoda volaná před prvním použitím paměti, tak se (jen kvůli invalidaci) instance v paměti nevytváří.
        /// </summary>
        /// <param name="klic">Klíč. Typicky 1=konfigurované texty.</param>
        /// <param name="skupina">Skupina. Je daná konkrétním případem (vizuální element).</param>
        public static void Invalidate(SqlInt32 skupina, string klic)
        {
            if (CurrentExists)
                Current._Invalidate(new ConfigKey(skupina, klic));
        }
        #endregion
        #region Private instanční výkonné metody
        /// <summary>
        /// Doplní do sebe data získaná z daného SQL příkazu.
        /// Volitelně zamkne seznam s daty.
        /// </summary>
        /// <param name="sql"></param>
        private void _AddData(string sql, bool setLock)
        {
            // Načtu záznamy z SQL:
            List<SchedulerConfigReadOnlyCls> items = Db_Layer.GetList<SchedulerConfigReadOnlyCls>(sql);

            // Sgrupuji načtená data podle klíče ConfigKey, a po jednotlivých skupinách je přidám do své instanční paměti:
            IEnumerable<IGrouping<ConfigKey, SchedulerConfigReadOnlyCls>> groups = items.GroupBy(item => new ConfigKey(item));
            if (setLock)
            {
                lock (this.Records)
                {
                    this._AddData(groups);
                }
            }
            else
                this._AddData(groups);
        }
        /// <summary>
        /// Doplní do sebe data z dodaného grupovaného seznamu.
        /// Tato metoda neřeší ani zámek lock (this.Records), ani původ dat. Prostě je jen přidá do this.Records.
        /// Pokud by v paměti byl nějaký prvek (grupa) pro klíč dat, který se vyskytuje v datech nyní načtených, pak záznam (celou grupu) pro daný klíč nejprve z paměti vyhodí.
        /// </summary>
        /// <param name="groups">Skupina grup = sgrupovaný soupis dat z SQL</param>
        private void _AddData(IEnumerable<IGrouping<ConfigKey, SchedulerConfigReadOnlyCls>> groups)
        {
			this.Version++;
            foreach (IGrouping<ConfigKey, SchedulerConfigReadOnlyCls> group in groups)
            {
                ConfigKey key = group.Key;
                if (this.Records.ContainsKey(key))
                    this.Records.Remove(key);

                this.Records.Add(key, new ConfiguredTexts(this.Version, group));
            }
        }
		/// <summary>
		/// Vrátí true, pokud pro danou skupinu a klíč dat je uvedená verze dat stále platná.
		/// Více viz property ConfiguredTexts.Version.
		/// </summary>
		/// <param name="key"></param>
		/// <param name="version"></param>
		/// <returns></returns>
		private bool _IsValid(ConfigKey key, int version)
		{
			ConfiguredTexts data = _GetData(key);
			if (data == null) return false;
			return (data.Version == version);
		}
        /// <summary>
        /// Najde (v paměti / v SQL serveru) data pro daný klíč a skupinu, a vrátí je jako seznam.
        /// Pokud data neexistují, vrátí prázdný list (aby bylo možno jej enumerovat bez testů).
        /// </summary>
        /// <param name="key">Klíč záznamu.</param>
        /// <returns></returns>
		private ConfiguredTexts _GetData(ConfigKey key)
        {
			ConfiguredTexts data;
            if (!this.Records.TryGetValue(key, out data))
            {   // Volně v paměti data nejsou:
                lock (this.Records)
                {   // Zamkneme si data (abychom nová data přidávali jen sami),
                    //  a podíváme se jestli (mezitím, než se aplikoval zámek) nám data někdo z jiného threadu nedoplnil:
                    if (!this.Records.TryGetValue(key, out data))
                    {   // Nikdo je nedoplnil, je to na nás:
                        this._AddData(SchedulerConfigReadOnlyCls.SqlSelect + " WHERE " + key.SqlFilter, false);
                        // Pokud jsme ale data nenašli ani my, tak vložíme do paměti záznam za daný klíč a do něj dáme null, to jako že data nejsou:
                        if (!this.Records.TryGetValue(key, out data))
                        {
                            data = null;
                            this.Records.Add(key, null);
                        }
                    }
                }
            }
            return data;
        }
        /// <summary>
        /// Zneplatní (zahodí) data daného klíče.
		/// Jakýkoli příští požadavek na data tohoto klíče způsobí, že se data načtou z SQL databáze do paměti v aktuální podobě (pod nové číslo verze).
        /// </summary>
        /// <param name="key"></param>
        private void _Invalidate(ConfigKey key)
        {
            lock (this.Records)
            {
                if (this.Records.ContainsKey(key))
                    this.Records.Remove(key);
            }
        }
        #endregion
        #region struct ConfigKey : klíč do group dictionary
        /// <summary>
        /// struct ConfigKey : klíč do group dictionary.
        /// Obsahuje hodnoty Klic a Skupina, neobsahuje hodnotu Typ.
        /// </summary>
        protected struct ConfigKey
        {
            public ConfigKey(int skupina, string klic)
            {
                this.Skupina = skupina;
                this.Klic = klic;
                this.Hash = 0;
                this.Hash = this._GetHash();
            }
            public ConfigKey(SqlInt32 skupina, string klic)
            {
                this.Skupina = (skupina.IsNull ? 0 : skupina.Value);
                this.Klic = klic;
                this.Hash = 0;
                this.Hash = this._GetHash();
            }
            public ConfigKey(SchedulerConfigReadOnlyCls record)
            {
                this.Skupina = (record.Skupina.IsNull ? 0 : record.Skupina.Value);
                this.Klic = record.Klic;
                this.Hash = 0;
                this.Hash = this._GetHash();
            }
            private int _GetHash()
            {
                return this.Skupina.GetHashCode() ^ (this.Klic == null ? 0 : this.Klic.GetHashCode());
            }
            ///<summary><para>Atribut: Skupina údaje. 1 = Konfigurovaný text</para><para>Db: lcs.scheduler_config.skupina (int null)</para></summary>
            internal int Skupina;
            ///<summary><para>Atribut: Klíč údaje. Pro konfigurované texty je zde uložen klíč konkrétního druhu elementů, pro který tato konfigurace platí. Klíč pochází z interface IConfiguredText, z property string ConfigurationKey.</para><para>Db: lcs.scheduler_config.klic (varchar (40) null)</para></summary>
            internal string Klic;
            /// <summary>
            /// Filtr do SQL příkazu. Obsahuje text bez WHERE a bez aliasu, například : "skupina=2 and klic='A010023q'" anebo "skupina=5 and klic is null".
            /// </summary>
            internal string SqlFilter
            {
                get
                {
                    string filter = "skupina=" + this.Skupina.ToString() + " and klic" + (this.Klic == null ? " is null" : "=" + SqlFormat.ToSql(this.Klic));
                    return filter;
                }
            }
            public override int GetHashCode()
            {
                return this.Hash;
            }
            public override bool Equals(object obj)
            {
                if (!(obj is ConfigKey)) return false;
                ConfigKey other = (ConfigKey)obj;
                return (this.Hash == other.Hash &&
                        this.Skupina == other.Skupina &&
                        this.Klic == other.Klic);
            }
            public override string ToString()
            {
                return "Skupina=" + this.Skupina + "; Klic=" + (this.Klic == null ? "{null}" : this.Klic);
            }
            private int Hash;
        }
        #endregion
    }
	#region class ConfiguredTexts : skupina položek záznamů SchedulerConfigReadOnlyCls + informace o skupině
	/// <summary>
	/// ConfiguredTexts : obsahuje soupis záznamů třídy SchedulerConfigReadOnlyCls (jedna definice textu s proměnnými) vždy za jednu skupinu dat,
	/// tj. obsahuje definice textů pro titulek tooltipu, pro text tooltipu a pro texty prvku (Captions).
	/// Navíc obsahuje informaci o číslu verze, viz property Version.
	/// </summary>
	public class ConfiguredTexts
	{
		internal ConfiguredTexts(int version, IEnumerable<SchedulerConfigReadOnlyCls> records)
		{
			this.Version = version;
			this.RecordCaptions = new List<SchedulerConfigReadOnlyCls>();

			foreach (SchedulerConfigReadOnlyCls record in records)
			{
				if (record.Typ.IsNull) continue;
				switch (record.Typ.Value)
				{
                    case Noris.Schedule.Support.Expressions.Constants.CONFIG_KLIC_TOOLTIP_TITLE:
						this.RecordToolTipTitle = record;
                        break;
                    case Noris.Schedule.Support.Expressions.Constants.CONFIG_KLIC_TOOLTIP_TEXT:
						this.RecordToolTipText = record;
                        break;
                    case Noris.Schedule.Support.Expressions.Constants.CONFIG_KLIC_CAPTION_SHORT:
                    case Noris.Schedule.Support.Expressions.Constants.CONFIG_KLIC_CAPTION_MEDIUM:
                    case Noris.Schedule.Support.Expressions.Constants.CONFIG_KLIC_CAPTION_LONG:
						this.RecordCaptions.Add(record);
                        break;
                    case Noris.Schedule.Support.Expressions.Constants.CONFIG_KLIC_VISUAL_SETTING:
                        this.RecordVisualSetting = record;
                        break;
				}
			}

            this.ToolTipTitle = (this.RecordToolTipTitle == null ? null : this.RecordToolTipTitle.Text);
            this.ToolTipText = (this.RecordToolTipText == null ? null : this.RecordToolTipText.Text);
            this.Captions = this.RecordCaptions.Select(rec => rec.Text).ToList();
            this.VisualSetting = new ConfiguredSetting((this.RecordVisualSetting != null && !String.IsNullOrEmpty(this.RecordVisualSetting.Text) ? this.RecordVisualSetting.Text : ConfiguredSetting.Default));
		}
		/// <summary>
		/// Číslo verze dat v tomto prvku.
		/// Po invalidaci dat dojde k jejich novému načtení, a při tom se navýší o 1 číslo verze.
		/// Každý grafický element si ve své property typu EvaluatedTexts pamatuje Version, podle které byl vytvořen.
		/// Pokud po vytvoření konkrétních textů dojde ke změně definice = uloží se záznam třídy SchedulerConfigCls (read/write třída),
		/// v háčku SaveAfter() dojde k invalidaci dat SchedulerConfigData.Invalidate(), zahodí se data z paměti (se starým číslem verze),
		/// a poté se načtou znovu (s novým číslem verze).
		/// Následně GUI při renderování tooltipů porovnává verzi uloženou v grafickém elementu proti verzi aktuální 
		/// (viz metoda SchedulerConfigData.IsValid()), a pokud se verze EvaluatedTexts.Version liší od ConfiguredTexts.Version,
		/// značí to že v paměti jsou texty podle již neplatné definice a musí se vytvořit aktuální.
		/// </summary>
		public int Version { get; private set; }
		/// <summary>
        /// Definice textu pro titulek tooltipu, celý záznam s daty SchedulerConfigReadOnlyCls, nebo null pokud neexistuje
		/// </summary>
		public SchedulerConfigReadOnlyCls RecordToolTipTitle { get; private set; }
		/// <summary>
        /// Definice textu pro obsah tooltipu, celý záznam s daty SchedulerConfigReadOnlyCls, nebo null pokud neexistuje
		/// </summary>
		public SchedulerConfigReadOnlyCls RecordToolTipText { get; private set; }
		/// <summary>
        /// Definice textů pro prvek (Captions), celé záznamy s daty SchedulerConfigReadOnlyCls, nebo empty list pokud žádný neexistuje
		/// </summary>
		public List<SchedulerConfigReadOnlyCls> RecordCaptions { get; private set; }
        /// <summary>
        /// Definice textu pro vizuální nastavení, celý záznam s daty SchedulerConfigReadOnlyCls, nebo null pokud neexistuje
        /// </summary>
        public SchedulerConfigReadOnlyCls RecordVisualSetting { get; private set; }
        /// <summary>
        /// Vzorec textu pro titulek tooltipu, string obsahující vzorec
        /// </summary>
        public string ToolTipTitle { get; private set; }
        /// <summary>
        /// Vzorec textu pro obsah tooltipu, string obsahující vzorec
        /// </summary>
        public string ToolTipText { get; private set; }
        /// <summary>
        /// Vzorec textů pro prvek (Captions), stringy obsahující vzorec
        /// </summary>
        public List<string> Captions { get; private set; }
        /// <summary>
        /// Vizuální nastavení, data
        /// </summary>
        public ConfiguredSetting VisualSetting { get; private set; }
	}
	#endregion
    #region class ConfiguredSetting : schránka na font (jeho stringové zadání, tvorba odpovídajícího fontu)
    public class ConfiguredSetting
    {
        #region Konstrukce, proměnné
        public ConfiguredSetting(string serial)
        {
            this.Serial = serial;
        }
        public ConfiguredSetting(Font font)
        {
            this.Serial = GetSerial(font);
        }
        public ConfiguredSetting(Font font, Color foreColor)
        {
            this.Serial = GetSerial(font, foreColor);
        }
        public ConfiguredSetting(Font font, Color foreColor, Color backColor)
        {
            this.Serial = GetSerial(font, foreColor, backColor);
        }
        public ConfiguredSetting(Font font, Color foreColor, Color backColor, ConfiguredVisibleMode visibleMode)
        {
            this.Serial = GetSerial(font, foreColor, backColor, visibleMode);
        }
        private string _Serial;
        private Font _Font;
        private Color _ForeColor;
        private Brush _ForeBrush;
        private Color _BackColor;
        private ConfiguredVisibleMode _VisibleMode;
        #endregion
        #region Public property
        public string Serial
        {
            get { this._CheckSerial(); return this._Serial; }
            set { this._ResetData(); this._Serial = value; this._CheckData(); }
        }
        /// <summary>
        /// Typ písma dle konfigurace
        /// </summary>
        public Font Font
        {
            get { this._CheckData(); return this._Font; }
            set { this._ResetSerial(); this._Font = value; }
        }
        /// <summary>
        /// Barva písma dle konfigurace
        /// </summary>
        public Color ForeColor
        {
            get { this._CheckData(); return this._ForeColor; }
            set { this._ResetSerial(); this._ForeBrush = null; this._ForeColor = value; }
        }
        /// <summary>
        /// Štětec pro kreslení písma dle konfigurace
        /// </summary>
        public Brush ForeBrush
        {
            get { this._CheckForeBrush(); return this._ForeBrush; }
        }
        /// <summary>
        /// Barva pozadí dle konfigurace, nevyužito
        /// </summary>
        public Color BackColor
        {
            get { this._CheckData(); return this._BackColor; }
            set { this._ResetSerial(); this._BackColor = value; }
        }
        /// <summary>
        /// Režim viditelnosti dle konfigurace
        /// </summary>
        public ConfiguredVisibleMode VisibleMode
        {
            get { this._CheckData(); return this._VisibleMode; }
            set { this._ResetSerial(); this._VisibleMode = value; }
        }
        #endregion
        #region Private vrstva: konverze serial - data a zpět
        private void _CheckSerial()
        {
            if (!String.IsNullOrEmpty(this._Serial)) return;
            this._Serial = GetSerial(this._Font, this._ForeColor, this._BackColor, this._VisibleMode);
        }
        private void _CheckData()
        {
            if (String.IsNullOrEmpty(this._Serial)) return;
            if (this._Font == null || this._ForeColor.IsEmpty || this._BackColor.IsEmpty || this._VisibleMode == ConfiguredVisibleMode.None)
            {
                string[] items = this._Serial.Split(SEPARATOR);
                if (items.Length >= 1)
                {
                    if (this._Font == null)
                        this._Font = (Font)Convertor.StringToFont(items[0]);
                }
                if (items.Length >= 2)
                {
                    if (this._ForeColor.IsEmpty)
                    {
                        this._ForeColor = (Color)Convertor.StringToColor(items[1]);
                        this._ForeBrush = null;
                    }
                }
                if (items.Length >= 3)
                {
                    if (this._BackColor.IsEmpty)
                        this._BackColor = (Color)Convertor.StringToColor(items[2]);
                }
                if (items.Length >= 4)
                {
                    if (this._VisibleMode == ConfiguredVisibleMode.None)
                        this._VisibleMode = (ConfiguredVisibleMode)Convertor.StringToInt32(items[3]);
                }
            }
        }
        private void _CheckForeBrush()
        {
            this._CheckData();
            if (this._ForeBrush == null)
            {
                if (!this._ForeColor.IsEmpty)
                    this._ForeBrush = new SolidBrush(this._ForeColor);
            }
        }
        private void _ResetSerial()
        {
            this._Serial = null;
        }
        private void _ResetData()
        {
            this._Font = null;
            this._ForeColor = Color.Empty;
            this._ForeBrush = null;
            this._BackColor = Color.Empty;
            this._VisibleMode = ConfiguredVisibleMode.None;
        }
        #endregion
        #region Static services (default, serializace)
        /// <summary>
        /// Obsahuje setting defaultního nastavení
        /// </summary>
        public static string Default
        {
            get { return GetSerial(SystemFonts.DefaultFont, Color.Black, Color.LightYellow, ConfiguredVisibleMode.Default); }
        }
        public static string GetSerial(Font font)
        {
            return Convertor.FontToString(font);
        }
        public static string GetSerial(Font font, Color foreColor)
        {
            return Convertor.FontToString(font) + SEPARATOR.ToString() +
                   Convertor.ColorToString(foreColor);
        }
        public static string GetSerial(Font font, Color foreColor, Color backColor)
        {
            return Convertor.FontToString(font) + SEPARATOR.ToString() +
                   Convertor.ColorToString(foreColor) + SEPARATOR.ToString() +
                   Convertor.ColorToString(backColor);
        }
        public static string GetSerial(Font font, Color foreColor, Color backColor, ConfiguredVisibleMode visibleMode)
        {
            return Convertor.FontToString(font) + SEPARATOR.ToString() +
                   Convertor.ColorToString(foreColor) + SEPARATOR.ToString() +
                   Convertor.ColorToString(backColor) + SEPARATOR.ToString() +
                   Convertor.Int32ToString((int)visibleMode);
        }
        public const char SEPARATOR = '\\';
        #endregion
    }
    /// <summary>
    /// Režim viditelnosti popisku prvku
    /// </summary>
    public enum ConfiguredVisibleMode
    {
        /// <summary>
        /// 0: Neurčeno
        /// </summary>
        None = 0,
        /// <summary>
        /// 1: Výchozí = podle deklarace grafu
        /// </summary>
        Default = 1,
        /// <summary>
        /// 2: Zobrazit vždy
        /// </summary>
        VisibleAllways = 2,
        /// <summary>
        /// 3: Zobrazit jen pokud je prvek aktivní (pod myší, selected, interactive)
        /// </summary>
        VisibleOnlyActive = 3,
        /// <summary>
        /// 4: Nezobrazovat
        /// </summary>
        Hidden = 4
    }
    #endregion
}
